 /*******************************************************************************
  * Copyright (c) 2000, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal.console;

 import java.io.IOException ;
 import java.util.ArrayList ;
 import java.util.Iterator ;
 import java.util.List ;

 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.OperationCanceledException;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.jobs.IJobManager;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.DocumentEvent;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IDocumentPartitionerExtension;
 import org.eclipse.jface.text.IRegion;
 import org.eclipse.jface.text.ITypedRegion;
 import org.eclipse.jface.text.Region;
 import org.eclipse.swt.custom.StyleRange;
 import org.eclipse.ui.console.ConsolePlugin;
 import org.eclipse.ui.console.IConsoleDocumentPartitioner;
 import org.eclipse.ui.console.IOConsole;
 import org.eclipse.ui.console.IOConsoleInputStream;
 import org.eclipse.ui.console.IOConsoleOutputStream;
 import org.eclipse.ui.progress.UIJob;
 import org.eclipse.ui.progress.WorkbenchJob;

 /**
  * Partitions an IOConsole's document
  * @since 3.1
  *
  */
 public class IOConsolePartitioner implements IConsoleDocumentPartitioner, IDocumentPartitionerExtension {
     private PendingPartition consoleClosedPartition;
     private IDocument document;
     private ArrayList partitions;
     /**
      * Blocks of data that have not yet been appended to the document.
      */
     private ArrayList pendingPartitions;
     /**
      * A list of PendingPartitions to be appended by the updateJob
      */
     private ArrayList updatePartitions;
     /**
      * The last partition appended to the document
      */
     private IOConsolePartition lastPartition;
     /**
      * Job that appends pending partitions to the document.
      */
     private QueueProcessingJob queueJob;
     /**
      * The input stream attached to this document.
      */
     private IOConsoleInputStream inputStream;
     /**
      * Flag to indicate that the updateJob is updating the document.
      */
     private boolean updateInProgress;
     /**
      * A list of partitions containing input from the console, that have
      * not been appended to the input stream yet.
      */
     private ArrayList inputPartitions;
     /**
      * offset used by updateJob
      */
     private int firstOffset;
     /**
      * An array of legal line delimiters
      */
     private String [] lld;
     private int highWaterMark = -1;
     private int lowWaterMark = -1;
     private boolean connected = false;

     private IOConsole console;
     
     private TrimJob trimJob = new TrimJob();
     /**
      * Lock for appending to and removing from the document - used
      * to synchronize addition of new text/partitions in the update
      * job and handling buffer overflow/clearing of the console.
      */
     private Object overflowLock = new Object ();
     
     
     private int fBuffer;
     
     public IOConsolePartitioner(IOConsoleInputStream inputStream, IOConsole console) {
         this.inputStream = inputStream;
         this.console = console;
         trimJob.setRule(console.getSchedulingRule());
     }
     
     public IDocument getDocument() {
         return document;
     }
     
     /*
      * (non-Javadoc)
      * @see org.eclipse.jface.text.IDocumentPartitioner#connect(org.eclipse.jface.text.IDocument)
      */
     public void connect(IDocument doc) {
         document = doc;
         document.setDocumentPartitioner(this);
         lld = document.getLegalLineDelimiters();
         partitions = new ArrayList ();
         pendingPartitions = new ArrayList ();
         inputPartitions = new ArrayList ();
         queueJob = new QueueProcessingJob();
         queueJob.setSystem(true);
         queueJob.setPriority(Job.INTERACTIVE);
         queueJob.setRule(console.getSchedulingRule());
         connected = true;
     }
     
     public int getHighWaterMark() {
         return highWaterMark;
     }
     
     public int getLowWaterMark() {
         return lowWaterMark;
     }
     
     public void setWaterMarks(int low, int high) {
         lowWaterMark = low;
         highWaterMark = high;
         ConsolePlugin.getStandardDisplay().asyncExec(new Runnable () {
             public void run() {
                 checkBufferSize();
             }
         });
     }
     
     /**
      * Notification from the console that all of its streams have been closed.
      */
     public void streamsClosed() {
         consoleClosedPartition = new PendingPartition(null, null);
         synchronized (pendingPartitions) {
             pendingPartitions.add(consoleClosedPartition);
         }
         queueJob.schedule(); //ensure that all pending partitions are processed.
 }
     
     /*
      * (non-Javadoc)
      * @see org.eclipse.jface.text.IDocumentPartitioner#disconnect()
      */
     public void disconnect() {
         synchronized (overflowLock) {
             document = null;
             partitions.clear();
             connected = false;
             try {
                 inputStream.close();
             } catch (IOException e) {
             }
         }
     }
     
     /*
      * (non-Javadoc)
      * @see org.eclipse.jface.text.IDocumentPartitioner#documentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent)
      */
     public void documentAboutToBeChanged(DocumentEvent event) {
     }
     
     /*
      * (non-Javadoc)
      * @see org.eclipse.jface.text.IDocumentPartitioner#documentChanged(org.eclipse.jface.text.DocumentEvent)
      */
     public boolean documentChanged(DocumentEvent event) {
         return documentChanged2(event) != null;
     }
     
     /*
      * (non-Javadoc)
      * @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes()
      */
     public String [] getLegalContentTypes() {
         return new String [] { IOConsolePartition.OUTPUT_PARTITION_TYPE, IOConsolePartition.INPUT_PARTITION_TYPE };
     }
     
     /*
      * (non-Javadoc)
      * @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int)
      */
     public String getContentType(int offset) {
         return getPartition(offset).getType();
     }
     
     /*
      * (non-Javadoc)
      * @see org.eclipse.jface.text.IDocumentPartitioner#computePartitioning(int, int)
      */
     public ITypedRegion[] computePartitioning(int offset, int length) {
         int rangeEnd = offset + length;
         int left= 0;
         int right= partitions.size() - 1;
         int mid= 0;
         IOConsolePartition position= null;
         
         if (left == right) {
             return new IOConsolePartition[]{(IOConsolePartition) partitions.get(0)};
         }
         while (left < right) {
             
             mid= (left + right) / 2;
                 
             position= (IOConsolePartition) partitions.get(mid);
             if (rangeEnd < position.getOffset()) {
                 if (left == mid)
                     right= left;
                 else
                     right= mid -1;
             } else if (offset > (position.getOffset() + position.getLength() - 1)) {
                 if (right == mid)
                     left= right;
                 else
                     left= mid +1;
             } else {
                 left= right= mid;
             }
         }
         
         
         List list = new ArrayList ();
         int index = left - 1;
         if (index >= 0) {
             position= (IOConsolePartition) partitions.get(index);
             while (index >= 0 && (position.getOffset() + position.getLength()) > offset) {
                 index--;
                 if (index >= 0) {
                     position= (IOConsolePartition) partitions.get(index);
                 }
             }
         }
         index++;
         position= (IOConsolePartition) partitions.get(index);
         while (index < partitions.size() && (position.getOffset() < rangeEnd)) {
             list.add(position);
             index++;
             if (index < partitions.size()) {
                 position= (IOConsolePartition) partitions.get(index);
             }
         }
         
         return (ITypedRegion[]) list.toArray(new IOConsolePartition[list.size()]);
     }

     
     /*
      * (non-Javadoc)
      * @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int)
      */
     public ITypedRegion getPartition(int offset) {
         for (int i = 0; i < partitions.size(); i++) {
             ITypedRegion partition = (ITypedRegion) partitions.get(i);
             int start = partition.getOffset();
             int end = start + partition.getLength();
             if (offset >= start && offset < end) {
                 return partition;
             }
         }
         
         if (lastPartition == null) {
             synchronized(partitions) {
                 lastPartition = new IOConsolePartition(inputStream, ""); //$NON-NLS-1$
 lastPartition.setOffset(offset);
                 partitions.add(lastPartition);
                 inputPartitions.add(lastPartition);
             }
         }
         return lastPartition;
     }
         
     /**
      * Enforces the buffer size.
      * When the number of lines in the document exceeds the high water mark, the
      * beginning of the document is trimmed until the number of lines equals the
      * low water mark.
      */
     private void checkBufferSize() {
         if (document != null && highWaterMark > 0) {
             int length = document.getLength();
             if (length > highWaterMark) {
                 if (trimJob.getState() == Job.NONE) { //if the job isn't already running
 trimJob.setOffset(length - lowWaterMark);
                     trimJob.schedule();
                 }
             }
         }
     }
     
     /**
      * Clears the console
      */
     public void clearBuffer() {
         synchronized (overflowLock) {
             trimJob.setOffset(-1);
             trimJob.schedule();
         }
     }
     
     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.jface.text.IDocumentPartitionerExtension#documentChanged2(org.eclipse.jface.text.DocumentEvent)
      */
     public IRegion documentChanged2(DocumentEvent event) {
         if (document == null) {
             return null; //another thread disconnected the partitioner
 }
         if (document.getLength() == 0) { //document cleared
 partitions.clear();
             inputPartitions.clear();
             pendingPartitions.clear();
             lastPartition = null;
             return new Region(0, 0);
         }
         
         
         if (updateInProgress) {
             synchronized(partitions) {
                 if (updatePartitions != null) {
                     for (Iterator i = updatePartitions.iterator(); i.hasNext(); ) {
                         PendingPartition pp = (PendingPartition) i.next();
                         if (pp == consoleClosedPartition) {
                             continue;
                         }
                         
                         int ppLen = pp.text.length();
                         if (lastPartition != null && lastPartition.getStream() == pp.stream) {
                             int len = lastPartition.getLength();
                             lastPartition.setLength(len + ppLen);
                         } else {
                             IOConsolePartition partition = new IOConsolePartition(pp.stream, ppLen);
                             partition.setOffset(firstOffset);
                             lastPartition = partition;
                             partitions.add(partition);
                         }
                         firstOffset += ppLen;
                     }
                 }
             }
         } else {// user input.
 int amountDeleted = event.getLength() ;
             
             if (amountDeleted > 0) {
                 int offset = event.fOffset;
                 IOConsolePartition partition = (IOConsolePartition) getPartition(offset);
                 if(partition == lastPartition) {
                     partition.delete(event.fOffset-partition.getOffset(), amountDeleted);
                 }
             }
             
             synchronized(partitions) {
                 if (lastPartition == null || lastPartition.isReadOnly()) {
                     lastPartition = new IOConsolePartition(inputStream, event.fText);
                     lastPartition.setOffset(event.fOffset);
                     partitions.add(lastPartition);
                     inputPartitions.add(lastPartition);
                 } else {
                     lastPartition.insert(event.fText, (event.fOffset-lastPartition.getOffset()));
                 }
                 
                 int lastLineDelimiter = -1;
                 String partitionText = lastPartition.getString();
                 for (int i = 0; i < lld.length; i++) {
                     String ld = lld[i];
                     int index = partitionText.lastIndexOf(ld);
                     if (index != -1) {
                         index += ld.length();
                     }
                     if (index > lastLineDelimiter) {
                         lastLineDelimiter = index;
                     }
                 }
                 if (lastLineDelimiter != -1) {
                     StringBuffer input = new StringBuffer ();
                     Iterator it = inputPartitions.iterator();
                     while (it.hasNext()) {
                         IOConsolePartition partition = (IOConsolePartition) it.next();
                         if (partition.getOffset() + partition.getLength() <= event.fOffset + lastLineDelimiter) {
                             if (partition == lastPartition) {
                                 lastPartition = null;
                             }
                             input.append(partition.getString());
                             partition.clearBuffer();
                             partition.setReadOnly();
                             it.remove();
                         } else {
                             //create a new partition containing everything up to the line delimiter
 //and append that to the string buffer.
 String contentBefore = partitionText.substring(0, lastLineDelimiter);
                             IOConsolePartition newPartition = new IOConsolePartition(inputStream, contentBefore);
                             newPartition.setOffset(partition.getOffset());
                             newPartition.setReadOnly();
                             newPartition.clearBuffer();
                             int index = partitions.indexOf(partition);
                             partitions.add(index, newPartition);
                             input.append(contentBefore);
                             //delete everything that has been appended to the buffer.
 partition.delete(0, lastLineDelimiter);
                             partition.setOffset(lastLineDelimiter + partition.getOffset());
                             lastLineDelimiter = 0;
                         }
                     }
                     if (input.length() > 0) {
                         inputStream.appendData(input.toString());
                     }

                 }
             }
         }
         
         return new Region(event.fOffset, event.fText.length());
     }
     
     private void setUpdateInProgress(boolean b) {
         updateInProgress = b;
     }
         
     /**
      * A stream has been appended, add to pendingPartions list and schedule updateJob.
      * updateJob is scheduled with a slight delay, this allows the console to run the job
      * less frequently and update the document with a greater amount of data each time
      * the job is run
      * @param stream The stream that was written to.
      * @param s The string that should be appended to the document.
      */
     public void streamAppended(IOConsoleOutputStream stream, String s) throws IOException {
         if (document == null) {
             throw new IOException ("Document is closed"); //$NON-NLS-1$
 }
         synchronized(pendingPartitions) {
             PendingPartition last = (PendingPartition) (pendingPartitions.size() > 0 ? pendingPartitions.get(pendingPartitions.size()-1) : null);
             if (last != null && last.stream == stream) {
                 last.append(s);
             } else {
                 pendingPartitions.add(new PendingPartition(stream, s));
                 if (fBuffer > 1000) {
                     queueJob.schedule();
                 } else {
                     queueJob.schedule(50);
                 }
             }
             
             if (fBuffer > 160000) {
                 try {
                     pendingPartitions.wait();
                 } catch (InterruptedException e) {
                 }
             }
         }
     }
     
     /**
      * Holds data until updateJob can be run and the document can be updated.
      */
     private class PendingPartition {
         StringBuffer text = new StringBuffer (8192);
         IOConsoleOutputStream stream;
         
         PendingPartition(IOConsoleOutputStream stream, String text) {
             this.stream = stream;
             if (text != null) {
                 append(text);
             }
         }
         
         void append(String moreText) {
             text.append(moreText);
             fBuffer += moreText.length();
         }
     }
     
     /**
      * Updates the document. Will append everything that is available before
      * finishing.
      */
     private class QueueProcessingJob extends UIJob {

         QueueProcessingJob() {
             super("IOConsole Updater"); //$NON-NLS-1$
 }
         
         /*
          * (non-Javadoc)
          * @see org.eclipse.core.internal.jobs.InternalJob#run(org.eclipse.core.runtime.IProgressMonitor)
          */
         public IStatus runInUIThread(IProgressMonitor monitor) {
             synchronized (overflowLock) {
                 ArrayList pendingCopy = new ArrayList ();
                 StringBuffer buffer = null;
                 boolean consoleClosed = false;
                 while (pendingPartitions.size() > 0) {
                     synchronized(pendingPartitions) {
                         pendingCopy.addAll(pendingPartitions);
                         pendingPartitions.clear();
                         fBuffer = 0;
                         pendingPartitions.notifyAll();
                     }

                     buffer = new StringBuffer ();
                     for (Iterator i = pendingCopy.iterator(); i.hasNext(); ) {
                         PendingPartition pp = (PendingPartition) i.next();
                         if (pp != consoleClosedPartition) {
                             buffer.append(pp.text);
                         } else {
                             consoleClosed = true;
                         }
                     }
                 }

                 if (connected) {
                     setUpdateInProgress(true);
                     updatePartitions = pendingCopy;
                     firstOffset = document.getLength();
                     try {
                         document.replace(firstOffset, 0, buffer.toString());
                     } catch (BadLocationException e) {
                     }
                     updatePartitions = null;
                     setUpdateInProgress(false);
                 }
                 if (consoleClosed) {
                     console.partitionerFinished();
                 }
                 checkBufferSize();

             }

             return Status.OK_STATUS;
         }
         
         /*
          * Job will process as much as it can each time it's run, but it gets
          * scheduled everytime a PendingPartition is added to the list, meaning
          * that this job could get scheduled unnecessarily in cases of heavy output.
          * Note however, that schedule() will only reschedule a running/scheduled Job
          * once even if it's called many times.
          */
         public boolean shouldRun() {
             boolean shouldRun = connected && pendingPartitions != null && pendingPartitions.size() > 0;
             return shouldRun;
         }
     }

  
     
     
     

     
     /**
      * Job to trim the console document, runs in the UI thread.
      */
     private class TrimJob extends WorkbenchJob {
         
         /**
          * trims output up to the line containing the given offset,
          * or all output if -1.
          */
         private int truncateOffset;
         
         /**
          * Creates a new job to trim the buffer.
          */
         TrimJob() {
             super("Trim Job"); //$NON-NLS-1$
 setSystem(true);
         }
         
         /**
          * Sets the trim offset.
          *
          * @param offset trims output up to the line containing the given offset
          */
         public void setOffset(int offset) {
             truncateOffset = offset;
         }

         /* (non-Javadoc)
          * @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
          */
         public IStatus runInUIThread(IProgressMonitor monitor) {
             IJobManager jobManager = Job.getJobManager();
             try {
                 jobManager.join(console, monitor);
             } catch (OperationCanceledException e1) {
                 return Status.CANCEL_STATUS;
             } catch (InterruptedException e1) {
                 return Status.CANCEL_STATUS;
             }
             if (document == null) {
                 return Status.OK_STATUS;
             }
             
             int length = document.getLength();
             if (truncateOffset < length) {
                 synchronized (overflowLock) {
                     try {
                         if (truncateOffset < 0) {
                             // clear
 setUpdateInProgress(true);
                             document.set(""); //$NON-NLS-1$
 setUpdateInProgress(false);
                             partitions.clear();
                         } else {
                             // overflow
 int cutoffLine = document.getLineOfOffset(truncateOffset);
                             int cutOffset = document.getLineOffset(cutoffLine);


                             // set the new length of the first partition
 IOConsolePartition partition = (IOConsolePartition) getPartition(cutOffset);
                             partition.setLength(partition.getOffset() + partition.getLength() - cutOffset);
                             
                             setUpdateInProgress(true);
                             document.replace(0, cutOffset, ""); //$NON-NLS-1$
 setUpdateInProgress(false);
                             
                             //remove partitions and reset Partition offsets
 int index = partitions.indexOf(partition);
                             for (int i = 0; i < index; i++) {
                                 partitions.remove(0);
                             }
                             
                             int offset = 0;
                             for (Iterator i = partitions.iterator(); i.hasNext(); ) {
                                 IOConsolePartition p = (IOConsolePartition) i.next();
                                 p.setOffset(offset);
                                 offset += p.getLength();
                             }
                         }
                     } catch (BadLocationException e) {
                     }
                 }
             }
             return Status.OK_STATUS;
         }
     }







     /* (non-Javadoc)
      * @see org.eclipse.ui.console.IConsoleDocumentPartitioner#isReadOnly(int)
      */
     public boolean isReadOnly(int offset) {
         return ((IOConsolePartition)getPartition(offset)).isReadOnly();
     }

     /* (non-Javadoc)
      * @see org.eclipse.ui.console.IConsoleDocumentPartitioner#computeStyleRange(int, int)
      */
     public StyleRange[] getStyleRanges(int offset, int length) {
         if (!connected) {
             return new StyleRange[0];
         }
         IOConsolePartition[] computedPartitions = (IOConsolePartition[])computePartitioning(offset, length);
         StyleRange[] styles = new StyleRange[computedPartitions.length];
         for (int i = 0; i < computedPartitions.length; i++) {
             int rangeStart = Math.max(computedPartitions[i].getOffset(), offset);
             int rangeLength = computedPartitions[i].getLength();
             styles[i] = computedPartitions[i].getStyleRange(rangeStart, rangeLength);
         }
         return styles;
     }
     
     
 }

